写一个[[005.干掉代码中的console.log|干掉代码中的console.log]] 的插件
仅用作原理学习和面试吹牛 B,真实项目请使用现成的轮子
想要亲自动手写一个 Webpack 的插件,那么不得不聊一下 Webpack 的构建过程。看看 plugin 是在什么阶段生效的,或者说 plugin 能生效的阶段都有哪些
Webpack 的构建过程可以分为以下几个主要步骤:(下方的内容会结合 create-react-app 这个脚手架去聊,以下简称为 CRA)
Webpack 通过 CLI 或 API 启动,并读取配置文件(如 webpack.config.js)。在 webpack.config.js 会存在判断当前环境的代码:
const isEnvDevelopment = webpackEnv === "development";
const isEnvProduction = webpackEnv === "production";
并且在文件内容中,很多配置的地方,都会找到一些三元表达式:
![[Pasted image 20250711102302.png]]
也就是说,同一个 plugin,根据不同的环境,可能存在不同的配置,这是为什么呢?
这其实就和 Webpack 的优化有关了。通过区分环境(开发环境、生产环境),来实现优化:
hot 模式,在试图重新加载整个页面之前,hot 模式会尝试使用 HMR 来更新HtmlWebpackPlugin 这个插件来说,我们只期望它在生产环境才去进行一些代码压缩的行为,因此通过三元表达式,根据环境来决定需要设置的配置参数。简单说一下配置项的作用:
removeComments:删除 HTML 中的注释collapseWhitespace:删除 HTML 中的空白字符removeRedundantAttributes:删除多余的属性useShortDoctype:使用简化的文档类型removeEmptyAttributes:删除空属性在 CRA 中,配置文件不止一个,还存在另一个 webpackDevServer.config.js 文件,它又是做什么用的?
它是用于配置开发服务器 webpack-dev-server 的行为。这里也拿一些配置项举几个例子,方便理解:
historyApiFallback:用于单页面应用,处理 HTML5 History API 路由问题publicPath:指定输出文件的公共路径compress:启用 gzip 压缩Webpack 初始化一个 Compiler 对象,该对象负责控制整个构建过程
Webpack 读取配置中的插件,并调用插件的 apply 方法,让插件可以注册钩子函数
一个 plugin 的基本结构:
class DemoPlugin {
// options: 接收 plugin 的配置项
constructor(options) {
// 获取配置项,初始化插件
}
// apply 是与 webpack 通信的桥梁
apply(compiler) {
// 获取 compiler,可以通过 compiler 对象访问 compilation 对象
}
}
apply 内部可以包含任何自定义逻辑,这些逻辑将在 Webpack 的特定生命周期钩子被触发时执行。插件可以利用这些钩子来修改构建结果、添加新的资产、或者执行其他任何必要的操作
比如:
apply(compiler) {
compiler.hooks.compile.tap('DemoPlugin', (compilationParams) => {
// 在编译器开始读取 Records 之前执行的操作
});
compiler.hooks.compilation.tap('DemoPlugin', (compilation) => {
// 在创建新的 compilation 之前执行的操作
});
}
Webpack 根据配置中的 entry 找到所有入口文件
每当检测到文件变化时,Webpack 都会创建一个新的 Compilation 对象,该对象包含了当前的模块资源、编译生成资源、变化的文件等
loader 加载器处理:在解析模块时,会使用配置中的 loader 对模块进行转换,例如通过 ts-loader 将 TypeScript 转换为 JavaScriptdist 文件夹完成阶段也可以让 plugin 介入,只要在 apply 中注册 hook 即可:
apply(compiler) {
// 注册完成阶段的钩子
compiler.hooks.done.tap('AfterBuildPlugin', (stats) => {
// 完成通知
console.log('Webpack build is finished!');
// 清理工作
this.cleanup();
});
}
Webpack 插件是一个具有 apply 方法的 JS 对象。 apply 方法会被 Webpack compiler 调用,并且在整个编译生命周期都可以访问 compiler 对象
// RemoveConsolePlugin.js
const pluginName = 'RemoveConsolePlugin';
class RemoveConsolePlugin {
// 由于不需要从外部传入 options
// 因此这里就不显示地定义 constructor 了
// constructor (options) {...}
apply(compiler) {
compiler.hooks.emit.tapAsync(
pluginName,
(compilation, callback) => {
Object.keys(compilation.assets).forEach((filename) => {
// 仅处理 .js 文件
if (filename.endsWith(".js")) {
const asset = compilation.assets[filename];
let content = asset.source();
// 使用正则表达式移除整个 console.log 语句
// 匹配 console.log( 之后的任意字符,直到遇到闭合的括号
const consoleLogRegex = new RegExp(
"console\\.log\\(.*?\\)",
"g"
);
const withoutConsole = content.replace(consoleLogRegex, "");
// 更新资源
compilation.assets[filename] = {
source: () => withoutConsole,
size: () => Buffer.byteLength(withoutConsole, "utf8"),
};
}
});
callback();
}
);
}
}
module.exports = RemoveConsolePlugin;
compiler hook 的 tap 方法的第一个参数,应该是大驼峰式命名的插件名称。建议为此使用一个常量,以便它可以在所有 hook 中重复使用
compiler.hooks.emit:可以通过 compiler 去获取一些 hook,在这里选择 emit 这个 hook:
![[Pasted image 20250711105004.png]]
在 asset 被输出到 output 之前,完成对 console.log 语句的删除
compilation 实例能够访问所有的模块和它们的依赖(大部分是循环依赖)。它会对应用程序的依赖图中所有模块,进行字面上的编译 (literal compilation)
在这里,通过 compilation 实例获取到 assets,这里面就存着所有被处理的文件了。考虑到插件的运行会影响打包的速度,这里仅对 .js 文件做删除 console.log 语句的处理
通过调用 asset.source() 来获取文件的源代码,之后就是很熟悉的字符串的正则匹配和替换了
回到 webpack.config.js 文件中,引入 RemoveConsolePlugin 并添加到插件数组中:
// webpack.config.js
const RemoveConsolePlugin = require("../src/RemoveConsolePlugin");
module.exports = {
// ...其他配置...
plugins: [
new RemoveConsolePlugin(),
// ...其他插件...
],
};
运行之后发现 ouput 有问题:
![[Pasted image 20250711112140.png]]
console.log 语句确实是被删除了,但是留下了一堆逗号,导致打包后的文件异常了
先取消使用自定义插件,再打包一次看看:
![[Pasted image 20250711112231.png]]
可以看到,输出的文件里,每一行 console.log 语句后面都跟着一个逗号。所以使用正则的方式去删除 console.log 语句,还得给正则表达式加上一个是否以逗号结尾的匹配规则:(,|$)
const consoleLogRegex = new RegExp(
"console\\.log\\(.*?\\)(,|$)",
"g"
);
这样就解决了这个问题
console 语句中的 log 而已,console 语句还有很多种类型,如 warn、debug 、info 等。由于我们的 plugin 不支持 options 的配置,在功能的全面性上较差,该如何改进?console 语句之外,还有其他方式吗?能结合 [[002.抽象语法树|AST]] 去做这件事情不?